﻿#region imports

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;

#endregion

namespace Codeplex.ODataTFS.Model.ExpressionVisitors
{
	public class WorkItemFilterExpressionVisitor : ExpressionVisitor
	{
		private readonly Expression _expression;
		private FilterNode _rootFilterNode;
		private readonly IDictionary<BinaryExpression, ExpressionType> _expressionsMap;
		private ISet<Expression> _ignorableExpressions;

		public WorkItemFilterExpressionVisitor(Expression expression)
		{
			_expression = expression;
			_expressionsMap = new Dictionary<BinaryExpression, ExpressionType>();
			_ignorableExpressions = new HashSet<Expression>();
		}

		public FilterNode Eval()
		{
			if (_expression == null)
			{
				return _rootFilterNode;
			}

			Visit(_expression);

			return _rootFilterNode;
		}

		protected override Expression VisitBinary(BinaryExpression node)
		{
			if (node == null)
			{
				throw new ArgumentNullException("node");
			}

			if (!_ignorableExpressions.Contains(node))
			{
				switch (node.NodeType)
				{
					case ExpressionType.Equal:
					case ExpressionType.NotEqual:
						AddEqualityFilterNode(node);
						break;
					case ExpressionType.GreaterThan:
					case ExpressionType.LessThan:
					case ExpressionType.GreaterThanOrEqual:
					case ExpressionType.LessThanOrEqual:
						AddInequalityFilterNode(node);
						break;
					case ExpressionType.OrElse:
					case ExpressionType.AndAlso:
						var field = node.Right as BinaryExpression;
						if (field != null)
						{
							_expressionsMap.Add(field, node.NodeType);
						}

						break;
				}
			}

			return base.VisitBinary(node);
		}

		private void AddInequalityFilterNode(BinaryExpression node)
		{
			var field = default(MemberExpression);
			var value = default(ConstantExpression);

			if (node.Left is MethodCallExpression)
			{
				field = (node.Left as MethodCallExpression).Arguments[0] as MemberExpression;
				value = (node.Left as MethodCallExpression).Arguments[1] as ConstantExpression;
			}
			else if (node.Left is MemberExpression && node.Right is ConstantExpression)
			{
				field = node.Left as MemberExpression;
				value = node.Right as ConstantExpression;
			}

			if (field != null && value != null)
			{
				if (!Constants.TFS.AllowedWorkItemProperties.Contains(field.Member.Name))
				{
					throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "You can only filter by the following properties: {0}. (e.g. /WorkItems/$filter=AssignedTo eq 'john' and  CreatedDate gt datetime'2010-10-18') ", string.Join(", ", Constants.TFS.AllowedWorkItemProperties.ToList())));
				}

				AddFilterNode(field.Member.Name, value.Value, FilterNode.ParseFilterExpressionType(node.NodeType), GetLogicalOperator(node));
			}
		}

		private void AddEqualityFilterNode(BinaryExpression node)
		{
			if (node.Left is MemberExpression && node.Right is ConstantExpression)
			{
				var field = node.Left as MemberExpression;
				var value = node.Right as ConstantExpression;

				if (!Constants.TFS.AllowedWorkItemProperties.Contains(field.Member.Name))
				{
					throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "You can only filter by the following properties: {0}. (e.g. /WorkItems/$filter=AssignedTo eq 'john' and  CreatedDate gt datetime'2010-10-18') ", string.Join(", ", Constants.TFS.AllowedWorkItemProperties.ToList())));
				}

				AddFilterNode(field.Member.Name, value.Value, FilterNode.ParseFilterExpressionType(node.NodeType), GetLogicalOperator(node));
			}
			else if (node.Left is ConditionalExpression && (node.Right is UnaryExpression || node.Right is ConstantExpression))
			{
				var leftExpression = node.Left as ConditionalExpression;

				var rightExpressionValue = true;
				var isRightExpressionConstantExpression = true;
				if (node.Right is UnaryExpression)
				{
					var rightExpression = node.Right as UnaryExpression;
					isRightExpressionConstantExpression = rightExpression.Operand is ConstantExpression;
					rightExpressionValue = (bool) (rightExpression.Operand as ConstantExpression).Value;
				}

				if (leftExpression.IfFalse is UnaryExpression && isRightExpressionConstantExpression)
				{
					var evaluateToTrue = rightExpressionValue ^ node.NodeType == ExpressionType.NotEqual;

					var falsePredicate = leftExpression.IfFalse as UnaryExpression;
					if (falsePredicate.Operand is MethodCallExpression)
					{
						var condition = falsePredicate.Operand as MethodCallExpression;
						if (condition.Method.Name.Equals("Contains", StringComparison.OrdinalIgnoreCase))
						{
							if (condition.Object is MemberExpression && condition.Arguments[0] is ConstantExpression)
							{
								_ignorableExpressions.Add(leftExpression.Test);

								var fieldName = (condition.Object as MemberExpression).Member.Name;
								var value = (condition.Arguments[0] as ConstantExpression).Value;

								AddFilterNode(fieldName, value, evaluateToTrue ? FilterExpressionType.Contains : FilterExpressionType.NotContains, GetLogicalOperator(node));
							}
						}
					}
				}
			}
		}

		private void AddFilterNode(string fieldName, object value, FilterExpressionType expressionType, FilterNodeRelationship filterNodeRelationship)
		{
			if ((_rootFilterNode != null) &&
				(_rootFilterNode.SingleOrDefault(p => p.Key.Equals(fieldName, StringComparison.OrdinalIgnoreCase)) != null))
			{
				if (!Constants.TFS.AllowedWorkItemPropertiesMultiples.Contains(fieldName))
				{
					throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "More than one filter expression found for attribute {0}. Only one filter expression per attribute is supported.", fieldName));
				}
			}

			var filterNode = new FilterNode {Key = fieldName, Value = (value == null) ? "null" : value.ToString(), Sign = expressionType, NodeRelationship = filterNodeRelationship};
			if (_rootFilterNode != null)
			{
				_rootFilterNode.AddNode(filterNode);
			}
			else
			{
				_rootFilterNode = filterNode;
			}
		}

		private FilterNodeRelationship GetLogicalOperator(BinaryExpression node)
		{
			var value = ExpressionType.AndAlso;
			_expressionsMap.TryGetValue(node, out value);

			if (value == ExpressionType.OrElse)
			{
				return FilterNodeRelationship.Or;
			}

			return FilterNodeRelationship.And;
		}
	}
}
